Core Data
Core Data とは?
特徴
undo/redo 機能がある
CloudKit との連携により、端末間の iCloud 同期が手軽に実装できる UIKit と連携し、View とモデルの同期が手軽に実装できる .xcdatamodeld ファイルにモデル定義を記述する。この記述のための GUI が Xcode に統合されている
概念
Core Data モデル
Core Data のモデル記述は Xcode 上で行うことが想定されていて、ファイル追加から "Data Model" を選択するとモデル記述ファイル作成できる。作成されるファイルの拡張子は .xcdatamodeld となる。 https://gyazo.com/c2f749a36873d848eb47fb53aac5b46b
主要なクラス
Core Data をコード上から取り扱うに当たって理解しておくべき概念について書いておく。Core Data を扱う上で触ることになるクラスとその関係は、下図のようになっている。 https://gyazo.com/7e0b07a226d131360b3be8a0ca2c1bf6
Context (NSManagedObjectContext) は Core Data において重要かつ複雑な存在で、実際に DB にデータを永続化する前の一時領域のような立ち位置となっている。Core Data でデータを DB から書き込む場合も読み込む場合も、まず Context 内にモデルを書き起こす必要がある。Context 内のモデルに変更を加えても即座に DB には反映されず、明示的に DB への永続化の指示を出す必要がある。 Store coordinator (NSPersistentStoreCoordinator) は、DB と直接アクセスして永続化データの取得/保存を行う。永続化データへのアクセスは必ず Store coordinator 経由で行う必要があり、Context も Store coordinator を経由してデータへのアクセスを行う。 Core Data を試す
Core Data モデルを記述する
Entity
まずは、Entity の定義から始める。Entity は Core Data で管理するオブジェクトの定義であり、モデルファイルに対し複数追加できる。RDBMS におけるテーブルのようなもので、実際裏側の SQLite ではテーブルとして管理されている。 Entity は、モデル記述エディタ左下の "Add Entity" から行える。最低限名前を定義してやれば良い。
Attributes
Entity に対して複数の Attributes を定義できる。RDBMS におけるカラムのようなもの。追加したい対象の Entity を選択した上で、Attributes 欄にアイテムを追加することで定義できる。最低限名前と型が定義されていれば良い。
https://gyazo.com/0d3ac2c84cff5fb988edc2257b030b47
Relationship
Core Data モデル記述のエディタにはグラフスタイルモードがあり、Ctrl + ドラッグで Entity 間に relationshp を作成できる。表スタイルの場合でも、Relationships に項目を追加することで relationship の作成は行える。 https://gyazo.com/b391b674bbd547efcadcb638c0841df5
relationship の重要な設定項目として、Delete rule と Cardinality Type がある。これらは relationship を選択した上で Xcode 右側のペインの Delete Rule, Type から各々設定できる。 https://gyazo.com/87848713ed3be711900d492ee4637f89
Delete Rule は、関連元の Entity が削除された際の関連先の Entity に対する挙動を定義する。設定可能な項目は以下がある。
table:Delete Rule
Delete Rule 説明
No Action 関連元 Entity が削除されたら、関連先 Entity から参照を取り除くのみ
Nullify 関連元 Entity が削除されたら、関連先 Entity からは null 参照になる
Cascade 関連元 Entity が削除されたら、全ての関連先 Entity を削除する
Deny 関連先 Entity が 1 つ以上存在したら、関連元 Entity の削除を拒否する
Cardinality Type は多重度のことであり、関連元 Entity に対して関連先 Entity がいくつ紐づくか?を定義する。設定可能な項目には以下がある。
table:Cardinality Type
Type 説明
To One 関連元 Entity は、1 つの関連先 Entity と紐づく
To Many 関連元 Entity は、複数の関連先 Entity と紐づく
Persistent Container を初期化する
iOS 10 未満だと、Core Data のためのセットアップをするだけでも一苦労だったのが、iOS 10 以降は Persistent Container のおかげでとても楽になった。以下のように初期化する。通常、Core Data の初期化はアプリの起動時に行う。 code:swift
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DataModel")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Unable to load persistent stores: \(error)")
}
}
return container
}()
}
モデルを読み書きする
Core Data を利用する準備が整ったので、実際にデータの読み書きを行うことができる。データの読み書きのためには、まずは Context を準備し、そこにモデルを書き起こす必要がある。 Persistent Container を初期化した時点で、viewContext という特別な Context がすでに用意されているので、試しにこれを読み書きの両方で利用する。これはその名前の通り View にデータを描画する用の Context なので、本来であれば書き込みを行う場合には別の Context を用意するのが良い。 Xcode によるコード自動生成
例えば、Tag という Entity をモデル定義に記述していた場合、以下のようなファイルが自動生成される。
code:swift
public class Tag: NSManagedObject {
}
さらに各 Attribute にアクセスするための以下のような拡張も自動生成される。
code:swift
public extension Tag {
@nonobjc
class func fetchRequest() -> NSFetchRequest<Tag> {
return NSFetchRequest<Tag>(entityName: "Tag")
}
@NSManaged var createdDate: Date?
@NSManaged var id: UUID?
@NSManaged var name: String?
@NSManaged var tsundocsCount: Int64
@NSManaged var updatedDate: Date?
@NSManaged var tsundocs: NSSet?
}
この自動生成された Entity のクラスを利用して、モデルの読み書きを行う。
モデルの書き込み
Core Data においてデータの読み書きのためには、まず Context 上にモデルを書き起こす必要がある。これは、書き起こしたい Entity の自動生成クラスを、書き起こし先の Context を用いて初期化することで行える。 書き起こした後は、そのインスタンスに対して自由に更新を行う。この更新時点では変更は永続化されず、Context 上にのみ反映される。永続化したい場合は、Context に対して明示的に save() メソッドを呼び出せば良い。
code:swift
// Context 上にモデルを生成
let tag = Tag(context: container.viewContext)
// モデルの更新
tag.id = UUID()
tag.name = "My tag name"
// 永続化
container.viewContext.save()
モデルの読み取り/更新
モデルの読み取りには NSFetchRequest を利用する。例えば、全ての Tag を取得したい場合には、以下のようにする。
code:swift
// リクエストの作成
let request: NSFetchRequest<Tag> = Tag.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(keyPath: \Tag.createdDate, ascending: false)
]
// リクエストの実行
let tags = try container.viewContext.fetch(request)
取得できたモデルは NSManagedObject を継承した Tag クラスのインスタンスとなっている。このインスタンスは Context 上で管理されており、インスタンスに書き込みを行うと、その変更はまず Context 上にのみ反映され、永続化はされない。
更新した後永続化したい場合は、Context に対して明示的に save() メソッドを呼び出せば良い。
code:swift
// NSManagedObject の更新
tags.forEach { tag in
tag.id = UUID()
}
// 永続化
container.viewContext.save()
詳解 Context
Tips
モデルオブジェクトのライフサイクルについて
モデルオブジェクトと Context は、各々互いに参照関係にあるけれど、その間は 弱参照 なので、生存期間にズレがある可能性がある。Context に常にモデルオブジェクトを強参照させたい場合は、retainsRegisteredObjects を利用する。生存期間が一時的になる予定のモデルオブジェクトを保持したい場合や、マルチスレッド間で値をやりとりしたい場合に解放されないようにするのに役立つ。 データの変更通知
モデルオブジェクトが更新されたら、それを通知して UI を動的に更新したいケースがある。このような場合に通知を受け取る手段は、大きく 2 つあるようだ。
NSFetchedResultsController による通知
NSFetchedResultsController は、fetch リクエストの結果を UI に反映するためのコントローラ であり、主に UITableView で利用されることを想定している I/F になっている。が、他の View や用途としても利用することができる。 UITableView と直接連携させる場合には専用の I/F が生えているのでそれを利用すれば良い。オプションとして delegate や cache を指定でき、指定した場合には振る舞いが変わる。 fetch リクエストを変更する場合は、キャッシュを利用している場合にはキャッシュを削除してから、再び performFetch() を呼び出す必要がある。
注意点として、relationship の更新はデフォルトだと通知されない というものがある。
Notification による通知
参考
コード自動生成
Core Data のオブジェクトをコードから扱う時に、以下のようなことをしたいケースがある。 Core Data オブジェクトを拡張してメソッドをはやし、ビジネスロジックを扱えるようにしたい これらの要求に合わせて、いくつかのコード自動生成オプションが選択できるようになっている。そもそも、Core Data に関連して自動生成されるコードは、クラス定義 と プロパティのアクセサ定義 の2種類になる。例えば、Quake という Entity からコード自動生成を行った場合、以下の 2 つのファイルを生成できる。 前者はクラス定義である。NSManagedObject のサブクラスが定義されている。クラス定義に直接メソッドやプロパティを拡張したい場合に手を加える。 code:Quake+CoreDataClass.swift
@objc(Quake)
public class Quake: NSManagedObject {
}
後者は、前者に対するプロパティ及びリレーション定義の拡張である。
code:Quake+CoreDataProperties.swift
extension Quake
@nonobjc public class func fetchRequest() -> NSFetchRequest<Quake> {
return NSFetchRequest<Quake>(entityName: "Quake")
}
@NSManaged public var magnitude: Float
@NSManaged public var place: String?
@NSManaged public var time: Date?
@NSManaged public var countries: NSSet?
}
// MARK: Generated accessors for countries
extension Quake {
@objc(addCountriesObject:)
@NSManaged public func addToCountries(_ value: Country)
@objc(removeCountriesObject:)
@NSManaged public func removeFromCountries(_ value: Country)
@objc(addCountries:)
@NSManaged public func addToCountries(_ values: NSSet)
@objc(removeCountries:)
@NSManaged public func removeFromCountries(_ values: NSSet)
}
コード生成に関するオプションは、Entity に対して行うことができ、以下の3種類から選択できる。
Class Definition
オブジェクトに対応するクラスに手を加える予定がない時に選択する
生成コードはプロジェクトのソースコードリストに現れず、ビルドプロセス時に生成される
Category/Extension
オブジェクトに対応するクラスを直接拡張したい場合に選択する
「Xcode > Editor > Create NSManagedObject Subclass...」 を選択し生成できる
Manual/None
オブジェクトに対応するクラスのプロパティを編集したい場合に選択する
「Xcode > Editor > Create NSManagedObject Subclass...」 を選択し生成できる (Category/Extension と同様)
マイグレーション
簡単な変更であれば、Lightweight Migration としてある程度自動でマイグレーションが可能。モデルファイルにバージョンを追加し、必要応じて次バージョンのモデルファイルを変更, 設定していく必要がある。
Lightweigt Migration の範囲外に及ぶような複雑なマイグレーションを行うこともできるようだ。
バイナリデータの保存
Core Data には BLOB が保存できるが、この時指定できるオプションに allows external storage というものがある。一般的に、BLOB は DB などに直接バイナリデータを保存するより、外部ストレージに保存してそこへの参照だけ DB に持たせた方が効率が良い。そうしないと DB サイズが肥大化してパフォーマンスに影響するし、変更の通知とも相性が悪い。 allows external storage を有効にすると、Core Data は保存しようとした BLOB のサイズに応じて、データを Core Data のバックエンド内に保存するか、ファイルシステム上に保存するかを変更してくれるようだ。 ===== 以下は WIP =====
Model は Entity のレシピを管理している
レシピをもとに実体を作成してそれを配置 & 操作するのは Context の中
同一の Entity (Managed Object) は Context 内に 1 つしか存在できない
ただし、複数 Context が存在した場合、各 Context 内に各々 1 つずつ同一の Entity は存在できる
1 つの Context 内に同一の Managed Object は 1 つしか存在できないことは保証されている
Context は Managed Object のライフサイクルを管理できる
Context からは ManagedObject を取り出したり、あるいは Persistent Store から fetch したりできる
Managed Object の変更を監視して、その変更を取り消したり、Persistent Store に永続化したりできる
外部の Store から取得されたオブジェクトは全て Context に登録され、グローバルな ID (NSManagedObjectID) が付与される
Context は Parent Store というものを持つ。これは、Context が持つ Managed Object の取得元、かつ変更のコミット先。
古くはこれは常に Persistent Store Coordinator だったが、現在は他の Context がある Context の Parent Store になれる
ただし、Root は必ず Persistent Store Coordinator になる
Parent Store が別の Cotnext だった場合、その別の context によって保存やフェッチが行われる。このようなことをするには以下のような理由がある
別のスレッドあるいはキューによってバックグラウンドで処理を行いたい
親 Context は、自身とは異なるスレッド/キューの子 Context からリクエストを受け取り処理できる
Thread Confinement?スレッド拘束?いずれにせよ、あるスレッドにオブジェクトを縛りつける制約のことを表現しているようだった。
Managed Object はこの Thread (serialized queue) Confinement を利用している。
ある Context でデータを commit しても、そのコミットは 1 つ上の store までしか伝播されない。root context に伝播するまでは永続化されない。逆に、親 Context は子 Context から変更を pull することはしない。
Context は通知を送信するけれど、必要な Context からのみ受け取るようにするべき
生成されたスレッドやキューに縛り付けられる
あるスレッド/キューで生成したコンテキストを別のスレッド/キューに受け渡してはいけない
代わりに、Persistent Store への参照を受け渡して新しく Context を作るべき
queue ベースの Context の setter はスレッドセーフなのでどのスレッドから呼び出しても良い
main スレッドで実行されている場合、main queue style の context は block based API を解さなくても呼び出せる
context の queue/スレッドで処理を実行したい場合には perform や performAndWait が利用できる
Context による Managed Object の操作
inserted/updated/deleted Objects で、変更が commit されていないオブジェクト群を取得できる
commit されていない変更があれば hasChanges フラグが立つ
save() で commit する
変更はスタックされていて、undo で変更単位で戻すことができる
rollback() で全ての変更をなしにできる
reset() は context 内の全ての managed object をリセットする。つまり登録解除するらしい。使いどきがわからない
Context 上には Managed Object がキャッシュされ、基本的には再度参照されても refetch はしない。そのため、別スレッドないし別 Context 経由でデータが変更されても気づけないかもしれない
stalenessInterval でキャッシュの生存期間を調整できる。デフォルトでは無限にキャッシュされる。0に設定することもできる。
mergePolicy について。メモリを優先するか Persistent Store を優先するか
良さそう
モデル定義
Model
Core Data の文脈で言うモデルとは、アプリケーション内で Core Data 経由で取り扱うオブジェクト構造を保持するモデルファイルのことを指す場合が多い。Xcode のファイル追加から「Data Model」を選択すると作成することができ、作成されるファイルの拡張子は .xcdatamodeld となる。 Entity
Core Data で管理するオブジェクトの定義。Model ファイルに対し複数追加できる。DB におけるテーブルのようなもの。設定可能な項目は以下のような感じ。 table:設定
項目 概要
Entity Name 名前
Abstract Entity インスタンス化せず、他のEntityの親EntityとなるだけのEntityとしたい場合にはチェックを入れる
Parent Entity プロパティを引き継ぐ親 Entity を指定できる
Class Name デフォルトでは Entity 名と同一だが、変更することもできる
Module モジュールに所属させることができる。デフォルトだとグローバル
Codegen コード生成。詳しくは別途記載
Spotlight Display Name Spotlight検索で見える名前を設定できる
User info Entity にひもづくアプリケーション特有の設定を追加できる
Versioning Hash Modifier バージョン管理参照
Versioning Renamig ID バージョン管理参照
https://gyazo.com/320522bd5919de9a8e8e3f9d60b9d58e
Attributes
Entity の保持するプロパティとして Attributes が定義できる。最低限名前と型が定義されていれば良い。
table:設定
設定項目 内容
Transient 直接永続化されず、計算などで導出できるプロパティ
Optional デフォルトはoptional。Swift の optional とは違い、required なプロパティも値が nil のタイミングがある
Default Value 生成時のデフォルト値。non-optional な型でデフォルト値を指定しておくとパフォーマンス上のメリットがある
Validation 例えば、数値の場合は最小値や最大値等の validation を設けることができる
Use Scalar Type 型によって、スカラー値か非スカラー値かを選べる。Double だったら Swift の Double か NSNumber か、みたいな
Preserve After Deletion ???
https://gyazo.com/b97f17ac04c7965d857daa9ae92ae52b
Core Data の Attributes には、典型的な RDBMS のように主キーという概念はない。バックエンドにはいくつかの種類をサポートしているが、特に SQLite がバックエンドに際には、主キーにあたる値は Core Data が自動で生成している。そのため、わざわざ Core Data のエンティティに一意に識別する用の ID を用意する必要はないという意見もある。 https://www.youtube.com/watch?v=8t6i94M0IXo
ただし、バックエンド内で完結している主キーとは別に、クライアント側でデータを扱う際にデータを一意に識別できた方が嬉しいケースもあると思う。例えば、UICollectionDiffableDataSource は、データが一意に識別できている必要がある。あるいは、とあるタイミングで特定のデータの取得をリクエストしたい場合がある。このような場合に、クライアント側で ID が取得できていないと困る。
Relationship
Core Data のモデルファイル編集 Editor には、グラフスタイルのエディタが存在する。Ctrl+ドラッグで、Entity 間に relation を作成することができる。 設定可能な項目は以下のような感じ
table: 設定
項目 内容
Transient 永続化されない relation
Optional 関連する対象 (destination) が 0 なら optional, 1以上であれば not optional
Destination 関連先
Inverse 変更を関連元と関連先の両方に伝播するために必要
Delete Rule 削除された際の挙動。後述
Cardinality Type 多重度。後述
Arrangement 関連に順序を持たせるかどうか
Count 関連数に上限と下限を設けることができる
Index in Spotlight Spotlight 検索で Index をはるか
Delete Rule は以下が設定できる
No Action
source が削除されたら、destination から参照を取り除くのみ
Nullify
source が削除されたら、destination は null 参照になる
Cascade
source が削除されたら、全ての destination を削除する
Deny
desrination が1つ以上存在したら、削除を拒否する
Cardinality Type は以下が設定できる。
To One
source は 1 つの destination とひもづく
To Many
source は複数の destination とひもづく
https://gyazo.com/87848713ed3be711900d492ee4637f89
Fetch Index Element
WWDC 2017 にて発表された。モデルのエディター上でEntityを追加する「+」ボタンを長押しすると作成できる。
Tips
マルチスレッド
Core Data はマルチスレッド環境でも動作するように設計されているが、全てのオブジェクトがスレッドセーフなわけではない。以下に気をつける必要がある。 Managed Object Context は、初期化時と同じスレッド/キューで利用する
Context から取得した Managed Object は、Context に紐づけられたキューから利用する
Configuration とは?
同一のモデルファイル内に、複数の Configuration を設定できる。各 Configuration には特定の Entity を所属させることができる。さらに、Configuration 毎に保存先の sqlite ファイルを別々にできる。
以下のようなユースケースが考えられる。
モデルの中で、Cloud に同期するデータと同期しないデータを分けたい
参考